﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Media;
using System.Xml;
using Kit3D.Xml;
using Path=System.IO.Path;

namespace Kit3D.Windows.Media.Media3D
{
    public class X3DModelVisual3D : ModelVisual3D
    {
        private string _source;

        private string _designTimePath;
        public string DesignTimePath
        {
            get { return _designTimePath; }
            set
            {
                _designTimePath = value;
                if (!_loaded && !string.IsNullOrEmpty(Source))
                    try
                    {
                        SetSource(Source); //try loading with the new design time path
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex);
                    }
            }
        }

        public string Source
        {
            get { return _source; }
            set { SetSource(value); }
        }

        private void SetSource(string source)
        {
            if (_loaded && string.Compare(_source, source, StringComparison.CurrentCulture) == 0)
                return;

            _source = source;
            LoadSource(); //if this class inherits from DependencyObject, this call will succeed, but if it is in the context of a Application.LoadComponent (in InitializeComponent)
                            //that call will throw a XamlParseException.  Don't know why, but it has something to do with the use of XmlReader and the size/structure of the file
                            //read by that XmlReader.  In this case, there is a potential workaround by foregoing the LoadSource in SetSource, and doing it lazily on the first 
                            //call to MeshGeometry3D.get_TriangleIndices or MeshGeometry3D.get_Positions -- this requires virtualizing those properties, though.

            //OnChanged();
            OnBecameDirty();
        }

        private bool _loaded;
        private void LoadSource()
        {
            _loaded = false;
            try
            {
                //string packUri = "/TestApp;component/CelestialBodiesDeclarative.xaml";
                var settings = new XmlReaderSettings()
                                   {
                                       DtdProcessing = DtdProcessing.Ignore,
                                       CheckCharacters = true,
                                       IgnoreComments = true,
                                       IgnoreWhitespace = true
                                   };
                using (var xmlReader = XmlReader.Create(Source, settings))
                {
                    LoadSource(xmlReader);
                }
                _loaded = true;
            }
            catch (XmlException)
            {
                if (string.IsNullOrEmpty(DesignTimePath))
                    return;

                //try Source from DesignTimePath
                var designTimeSource = Path.Combine(DesignTimePath, Source);
                if (!File.Exists(designTimeSource))
                    throw new FileNotFoundException(designTimeSource);
                using (var fs = File.OpenRead(designTimeSource))
                {
                    using (var xmlReader = XmlReader.Create(fs, new XmlReaderSettings() { DtdProcessing = DtdProcessing.Ignore }))
                    {
                        LoadSource(xmlReader);
                    }
                }

                _loaded = true;
            }
        }

        private void LoadSource(XmlReader xmlReader)
        {
            var models = new Model3DGroup();
            var modelCollection = new Model3DCollection();

            var xmlDocument = new XmlDocument();
            xmlDocument.Load(xmlReader);
            var collisionElements = GetCollisionElements(xmlDocument);
            foreach (var collisionElement in collisionElements)
            {
                foreach (var transform in collisionElement.GetChildren("Transform"))
                {
                    var model = new GeometryModel3D();
                    
                    var geometry = new MeshGeometry3D();

                    var newPositions = new Point3DCollection();
                    var newTriangleIndices = new Int32Collection();

                    var translationString = transform.Attributes["translation"].Trim();
                    var scaleString = transform.Attributes["scale"].Trim();
                    var rotationString = transform.Attributes["rotation"].Trim();

                    var vals = GetPoint3D(translationString);
                    var translation = new TranslateTransform3D(vals.X, vals.Y, vals.Z);

                    vals = GetPoint3D(scaleString);
                    var scale = new ScaleTransform3D(vals.X, vals.Y, vals.Z);

                    var rotation = GetRotateTransform3D(rotationString);

                    var transformGroup = new Transform3DGroup();
                    transformGroup.Children.Add(translation);
                    transformGroup.Children.Add(scale);
                    transformGroup.Children.Add(rotation);

                    foreach (var shape in transform.GetChildren("Shape"))
                    {
                        var shapeBaseIndex = newPositions.Count;
                        XmlElement indexedFaceSet = null;
                        foreach (var child in shape.Children)
                        {
                            if (child.Name == "IndexedFaceSet")
                            {
                                indexedFaceSet = child;
                            }
                        }
                        if (indexedFaceSet == null)
                            return;

                        foreach (var index in GetTriangleIndices(indexedFaceSet, shapeBaseIndex))
                            newTriangleIndices.Add(index);
                        foreach (var position in GetPositions(indexedFaceSet))
                            newPositions.Add(position);

                        geometry.Positions = newPositions;
                        geometry.TriangleIndices = newTriangleIndices;

                        model.Geometry = geometry;
                        model.Transform = transformGroup;
                        //TODO: Parse / construct materials
                        model.Material = new DiffuseMaterial(new Kit3DBrush(new SolidColorBrush(Colors.Yellow)));

                        modelCollection.Add(model);
                    }
                }
            }

            models.Children = modelCollection;

            Content = models;
        }

        private static Point3DCollection GetPositions(XmlElement indexedFaceSet)
        {
            var positions = new Point3DCollection();

            var coordinate = indexedFaceSet.Children[0];
            var pointList = coordinate.Attributes["point"];
            var pointStrings = pointList.Split(new string[] {", "}, StringSplitOptions.None);
            foreach (var pointString in pointStrings)
            {
                var trimPointString = pointString.Trim();
                if (string.IsNullOrEmpty(trimPointString))
                    continue;
                positions.Add(GetPoint3D(trimPointString));
            }

            return positions;
        }

        private static Point3D GetPoint3D(string pointString)
        {
            var valueStrings = pointString.Split(' ');
            double x, y, z;
            if (!double.TryParse(valueStrings[0], out x))
                throw new ArgumentException(string.Format("x value {0} must be a double", valueStrings[0]));
            if (!double.TryParse(valueStrings[1], out y))
                throw new ArgumentException(string.Format("y value {0} must be a double", valueStrings[1]));
            if (!double.TryParse(valueStrings[2], out z))
                throw new ArgumentException(string.Format("z value {0} must be a double", valueStrings[2]));
            return new Point3D(x, y, z);
        }

        private static RotateTransform3D GetRotateTransform3D(string rotationText)
        {
            var valueStrings = rotationText.Split(' ');
            double x, y, z, angle;
            if (!double.TryParse(valueStrings[0], out x))
                throw new ArgumentException(string.Format("x value {0} must be a double", valueStrings[0]));
            if (!double.TryParse(valueStrings[1], out y))
                throw new ArgumentException(string.Format("y value {0} must be a double", valueStrings[1]));
            if (!double.TryParse(valueStrings[2], out z))
                throw new ArgumentException(string.Format("z value {0} must be a double", valueStrings[2]));
            if (!double.TryParse(valueStrings[3], out angle))
                throw new ArgumentException(string.Format("angle value {0} must be a double", valueStrings[3]));

            var rotation = new AxisAngleRotation3D(new Vector3D(x, y, z), GetDegrees(angle));
            return new RotateTransform3D(rotation);
        }

        private static double GetDegrees(double radians)
        {
            return ((180 / System.Math.PI) * radians);
        }

        private Int32Collection GetTriangleIndices(XmlElement indexedFaceSet, int shapeBaseIndex)
        {
            var triangleIndices = new Int32Collection();

            var indexList = indexedFaceSet.Attributes["coordIndex"];
            var indexGroups = indexList.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries);
            foreach (var indexGroup in indexGroups)
            {
                if (string.IsNullOrEmpty(indexGroup.Trim()))
                    continue;
                var indexStrings = indexGroup.Trim().Split(' ');

                var faceIsQuad = false;
                if (indexStrings.Length == 5)
                    faceIsQuad = true;
                else if (indexStrings.Length > 5)
                    throw new Exception("Polyons > 4 sides not supported (only triangles and quadrangles");
                
                var faceIndices = new Int32Collection();
                foreach (var indexString in indexStrings)
                {
                    var index = int.Parse(indexString);
                    if (index == -1)
                        break;
                    faceIndices.Add(shapeBaseIndex + index);
                }

                if (faceIsQuad)
                    faceIndices = GetTriangleIndices(faceIndices);

                foreach (var index in faceIndices)
                    triangleIndices.Add(index);
            }

            return triangleIndices;
        }

        public Int32Collection GetTriangleIndices(Int32Collection quadrangleIndices)
        {
            if (quadrangleIndices.Count % 4 != 0)
                throw new ArgumentException(string.Format("quadrangle index count {0} must be multiple of 4", quadrangleIndices.Count));

            var triangleIndices = new Int32Collection();

            for (var i = 0; i < quadrangleIndices.Count; i += 4)
            {
                //0,1,2,2,3,0
                var idx0 = quadrangleIndices[i];
                var idx2 = quadrangleIndices[i + 2];
                triangleIndices.Add(idx0);
                triangleIndices.Add(quadrangleIndices[i + 1]);
                triangleIndices.Add(idx2);
                triangleIndices.Add(idx2);
                triangleIndices.Add(quadrangleIndices[i + 3]);
                triangleIndices.Add(idx0);
            }

            return triangleIndices;
        }

        private static List<XmlElement> GetCollisionElements(XmlDocument xmlDocument)
        {
            var elements = new List<XmlElement>();
            
            if (xmlDocument == null)
                return elements;

            var x3d = xmlDocument.DocumentElement;
            if (x3d == null)
                throw new Exception("Invalid X3D file format - root X3D element missing");

            foreach (var scene in x3d.GetChildren("Scene"))
            {
                foreach (var collision in scene.GetChildren("Collision"))
                {
                    elements.Add(collision);
                }
            }

            return elements;
        }
    }
}
